package at.codebase.web.util.servlet;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Map;
import java.util.logging.Logger;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import at.codebase.core.ServletList;
import at.codebase.core.Settings;
import at.codebase.core.TokenPool;
import at.codebase.security.SecurePrintWriter;
import at.codebase.security.SecureStringBuilder;
import at.codebase.web.util.LocalRequest;
import at.codebase.web.util.LocalRequest.RequestInformation;
import at.codebase.web.util.URLParameters.ParameterDoesNotExistException;

import com.google.appengine.api.users.UserServiceFactory;

/**
 * This class is the secure alternative to the common HttpServlet class. It
 * should be used throughout the application and be the base class of every
 * "normal" or AJAX servlet.
 * 
 * @author Daniel Marth <danielmarth@gmx.at>
 */
public abstract class SecureServlet extends HttpServlet {

	private static final Logger log = Logger.getLogger(SecureServlet.class
			.getName());

	private SecurePrintWriter secureWriter = new SecurePrintWriter();
	private ArrayList<UserLevel> securityLevels = new ArrayList<UserLevel>();
	private HttpServletResponse response;
	private HttpServletRequest request;
	private TokenPool tokenPool;

	public static enum RequestType {
		POST, GET, POST_AND_GET
	}

	public static enum UserLevel {
		ADMIN, REGISTERED, ANONYMOUS
	}

	public SecureServlet() {	
		initSecurityLevels(securityLevels);

		if (securityLevels.size() == 0)
			log.warning(SecureServlet.class.getName()
					+ " has no SecurityLevels defined.");
	}

	@Override
	public final void doGet(HttpServletRequest req, HttpServletResponse resp) {
		updateSecureWWriter(resp);
		if (requestType().equals(RequestType.GET)
				|| requestType().equals(RequestType.POST_AND_GET)) {
			processRequest(req, resp, RequestType.GET);
		} else {
			writer().printSecureLine("Request method 'GET' not supported");
		}
	}

	@Override
	public final void doPost(HttpServletRequest req, HttpServletResponse resp) {
		updateSecureWWriter(resp);
		if (requestType().equals(RequestType.POST)
				|| requestType().equals(RequestType.POST_AND_GET)) {
			processRequest(req, resp, RequestType.POST);
		} else {
			writer().printSecureLine("Request method 'POST' not supported");
		}
	}

	private void processRequest(HttpServletRequest req,
			HttpServletResponse resp, RequestType requestType) {
		if(Settings.getServerAddress() == null) {
			Settings.setServerAddress(req);
		}
		request = req;
		response = resp;
		try {
			request.setCharacterEncoding("UTF-8");
			response.setCharacterEncoding("UTF-8"); // FIXME Test
		} catch (UnsupportedEncodingException e) {
			// FIXME
		}
		
		response.setContentType(getContentType());     // FIXME
		
		updateTokenPool();
		RequestInformation reqInfo = LocalRequest.create(request);
		UserLevel level = getUserLevel(reqInfo);
		if (!securityLevels.contains(level)) {
			processRequestNoAccess(reqInfo, level);
		} else {
			processRequestSecurityLevel(reqInfo, level, requestType);
		}
		writer().flushClose();
	}

	private void updateTokenPool() {
		tokenPool = (TokenPool) request.getSession().getAttribute("xsrf_token");
		if (tokenPool == null) {
			tokenPool = new TokenPool();
			request.setAttribute("xsrf_token", tokenPool);
		}
	}

	private void updateSecureWWriter(HttpServletResponse resp) {
		try {
			writer().setWriter(resp.getWriter());
		} catch (IOException e) {
			redirectTo(ServletList.ErrorHome);
		}
	}

	private UserLevel getUserLevel(RequestInformation reqInfo) {
		UserLevel level = UserLevel.ANONYMOUS;
		if (reqInfo.isUserLoggedIn()) {
			level = UserLevel.REGISTERED;
			if (reqInfo.getUser().isAdmin()
			// TODO: REMOVE THIS!!! ONLY FOR TESTING??! SETS ADMIN AUTOMATICALLY
			// IF ITS AN APP ADMIN
			|| (UserServiceFactory.getUserService().isUserLoggedIn() &&
			UserServiceFactory .getUserService().isUserAdmin())) {
				level = UserLevel.ADMIN;
			}
		}
		return level;
	}

	protected SecurePrintWriter writer() {
		return secureWriter;
	}

	protected String nextToken() {
		String token = tokenPool.generate();
		request.getSession().setAttribute("xsrf_token", tokenPool);
		return token;
	}
	
	protected boolean useToken(final String t) {
		return tokenPool.remove(t);
	}
	
	protected boolean tokenCorrect(RequestInformation requestInfo) {
		String token = null;
		try {
			token = requestInfo.getParameters().getString("randomKey");
		} catch (ParameterDoesNotExistException e1) {
			// TODO Auto-generated catch block
		}
		if(token == null || !useToken(token)) {
			return false;
		}
		return true;
	}

	/**
	 * Sets the security levels for that specific servlet
	 */
	abstract protected void initSecurityLevels(ArrayList<UserLevel> secLevels);

	/**
	 * Override this Method to set your own RequestType
	 */
	@SuppressWarnings("static-method")
	protected RequestType requestType() {
		return RequestType.POST_AND_GET;
	}

	abstract protected void processRequestSecurityLevel(
			RequestInformation requestInfo, final UserLevel level,
			final RequestType requestType);

	@SuppressWarnings("unused")
	protected void processRequestNoAccess(RequestInformation requestInfo,
			final UserLevel level) {
		writer().printInsecure("<h1>ACCESS DENIED</h1>");
	}

	public void redirectTo(final String url) {
		try {
			response.sendRedirect(url);
		} catch (Exception e) {
			response.setHeader(
					"Location",
					new SecureStringBuilder()
							.appendInsecure(request.getContextPath())
							.appendInsecure(url).toString());
		}
	}

	@SuppressWarnings("unchecked")
	public void forwardTo(final String url, Map<String, String> parameters) {
		try {
			request.getParameterMap().clear();
			request.getParameterMap().putAll(parameters);
			request.getRequestDispatcher(url).forward(request, response);
		} catch (Exception e) {
			response.setHeader(
					"Location",
					new SecureStringBuilder()
							.appendInsecure(request.getContextPath())
							.appendInsecure(url).toString());
		}
	}

	@SuppressWarnings("static-method")
	protected String getContentType(){
		return "text/html";
	}
	
}
